﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.ServiceModel.Web;
using System.Text;
using System.Threading;
using System.Web.Configuration;
using System.Xml;

namespace SavedSearches
{
    public class SavedSearchesService : ISavedSearchesService
    {
        /// <summary>
        /// Lock for the xml file.
        /// </summary>
        private Mutex mutex = new Mutex(false, WebConfigurationManager.AppSettings["saved.search.mutex.key"]);

        /// <summary>
        /// Return the filename, localized for the current domain.
        /// </summary>
        /// <returns>the XML database filename</returns>
        private string GetFileName()
        {
            // Get the base directory
            string baseDirectory = AppDomain.CurrentDomain.BaseDirectory;
            // append the configuration value for the xml filename
            return baseDirectory + "\\" + WebConfigurationManager.AppSettings["saved.search.xml.filename"];
        }

        /// <summary>
        /// Read and return the XML database document for saved searches.
        /// </summary>
        /// <returns>the XML database document for saved searches</returns>
        public XmlDocument ReadDocument()
        {
            // Create the document
            XmlDocument document = new XmlDocument();
            try
            {
                // load the file contents
                document.Load(this.GetFileName());
            }
            catch (FileNotFoundException)
            {
                // The file doesn't exist yet. Load it in memory
                document.AppendChild(document.CreateNode(XmlNodeType.XmlDeclaration, "", ""));
                document.AppendChild(document.CreateElement("savedSearches"));
            }
            return document;
        }

        /// <summary>
        /// Save the given document
        /// </summary>
        /// <param name="doc">the document to save</param>
        public void WriteDocument(XmlDocument doc)
        {
            doc.Save(this.GetFileName());
        }

        public XmlNode CreateUserSearchNode(XmlDocument doc, string user, string query)
        {
            // Create a <user> node containing the user text
            XmlNode userNode = doc.CreateElement("user");
            userNode.AppendChild(doc.CreateTextNode(user));
            
            // Create a <query> node containing the query text
            XmlNode queryNode = doc.CreateElement("query");
            queryNode.AppendChild(doc.CreateTextNode(query));

            // Create a <search> node and append the children
            XmlNode userSearchNode = doc.CreateElement("search");
            userSearchNode.AppendChild(userNode);
            userSearchNode.AppendChild(queryNode);

            // Return the created node
            return userSearchNode;
        }

        public int SaveUserSearch(string user, string query)
        {
            // assume we didn't find any searches
            int result = 0;
            // wait for a lock on the file
            mutex.WaitOne();
            try
            {
                // read the document
                XmlDocument document = this.ReadDocument();
                // create the new search node
                XmlNode userSearchNode = this.CreateUserSearchNode(document, user, query);
                // add the node to the savedSearches node
                document.GetElementsByTagName("savedSearches")[0].AppendChild(userSearchNode);
                // set result equal to the number of searches we have saved for the user
                result = new List<XmlNode>(this.GetUserSearchNodes(document, user)).Count;
                // save the document
                this.WriteDocument(document);
            }
            finally
            {
                mutex.ReleaseMutex();
            }
            return result;
        }

        /// <summary>
        /// Helper iterator to loop over &lt;search&gt; nodes.
        /// </summary>
        /// <param name="doc">the document to search</param>
        /// <returns>an iterable over all the search nodes</returns>
        private IEnumerable<XmlNode> GetSearches(XmlDocument doc)
        {
            // loop over all the search nodes
            foreach (XmlNode node in doc.GetElementsByTagName("search"))
            {
                // yield the search node
                yield return node;
            }
        }

        /// <summary>
        /// Helper iterator to enable looping over search nodes for a particular user.
        /// </summary>
        /// <param name="doc">the document to search</param>
        /// <param name="user">the user name</param>
        /// <returns></returns>
        private IEnumerable<XmlNode> GetUserSearchNodes(XmlDocument doc, string user)
        {
            // loop over all the search nodes
            foreach (XmlNode searchNode in this.GetSearches(doc))
            {
                // loop over all the children nodes, we're looking for the user node
                foreach (XmlNode childNode in searchNode.ChildNodes)
                {
                    // if we find the user node, and the node's text maches the user, yield the node
                    if (childNode.Name.Equals("user") && childNode.InnerText.Equals(user))
                    {
                        yield return searchNode;
                    }
                }
            }
        }

        /// <summary>
        /// Helper iterator to allow easy iteration over all the queries saved for a particular user
        /// </summary>
        /// <param name="doc">the document to search</param>
        /// <param name="user">the username to search for</param>
        /// <returns>iterator over the query strings saved for the given user</returns>
        private IEnumerable<string> GetUserSearchQueries(XmlDocument doc, string user)
        {
            // loop over all the search nodes for the given user
            foreach (XmlNode userSearchNode in this.GetUserSearchNodes(doc, user))
            {
                // loop over the children, we are looking for the query node
                foreach (XmlNode childNode in userSearchNode.ChildNodes)
                {
                    // if it is a query node, yield the text inside the tag
                    if (childNode.Name.Equals("query"))
                    {
                        yield return childNode.InnerText;
                    }
                }
            }
        }

        public string[] GetSearchesForUser(string user)
        {
            // create an empty results collection
            List<string> results = new List<string>();
            // acquire the lock
            mutex.WaitOne();
            try
            {
                // read the document
                XmlDocument document = this.ReadDocument();
                // fetch all the user query strings
                results = new List<string>(this.GetUserSearchQueries(document, user));
            }
            finally
            {
                // release the lock on the file
                mutex.ReleaseMutex();
            }
            // convert the results to an array
            return results.ToArray();
        }

        public IEnumerable<string> GetUserNames(XmlDocument doc)
        {
            // loop over all the user nodes
            foreach (XmlNode userNode in doc.GetElementsByTagName("user"))
            {
                // yield the text inside the tag
                yield return userNode.InnerText;
            }
        }

        public string[] GetAllUsers()
        {
            // create an empty results array
            HashSet<string> results = new HashSet<string>();
            // acquire the lock
            mutex.WaitOne();
            try
            {
                // read the document
                XmlDocument document = this.ReadDocument();
                // read all the users and get the unique set of them
                results = new HashSet<string>(this.GetUserNames(document));
            }
            finally
            {
                // release the lock on the database
                mutex.ReleaseMutex();
            }
            // return the results as an array
            return results.ToArray();
        }
    }
}
